package auth.plugins.flutter.io.fist_local_auth;

import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.biometric.BiometricConstants;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;

import java.lang.annotation.Retention;
import java.security.Signature;
import java.util.concurrent.Executor;

import javax.crypto.Cipher;
import javax.crypto.Mac;

import static java.lang.annotation.RetentionPolicy.SOURCE;
@RequiresApi(api = Build.VERSION_CODES.M)
public class BiometricPromptV2 {
    private static final String TAG = "BiometricPromptCompatV2";
    private static final boolean DEBUG = false;
    // In order to keep consistent behavior between versions, we need to send
    // FingerprintDialogFragment a message indicating whether or not to dismiss the UI instantly.
    private static final int DELAY_MILLIS = 500;

    static final String DIALOG_FRAGMENT_TAG = "FingerprintDialogFragmentV2";
    static final String FINGERPRINT_HELPER_FRAGMENT_TAG = "FingerprintHelperFragmentV2";
    static final String BIOMETRIC_FRAGMENT_TAG = "BiometricFragmentV2";
    static final String KEY_TITLE = "title";
    static final String KEY_SUBTITLE = "subtitle";
    static final String KEY_DESCRIPTION = "description";
    static final String KEY_NEGATIVE_TEXT = "negative_text";
    static final String KEY_FAIL_HINT="failhint";
    private Cipher mCipher;
    @Retention(SOURCE)
    @IntDef({BiometricConstants.ERROR_HW_UNAVAILABLE,
            BiometricConstants.ERROR_UNABLE_TO_PROCESS,
            BiometricConstants.ERROR_TIMEOUT,
            BiometricConstants.ERROR_NO_SPACE,
            BiometricConstants.ERROR_CANCELED,
            BiometricConstants.ERROR_LOCKOUT,
            BiometricConstants.ERROR_VENDOR,
            BiometricConstants.ERROR_LOCKOUT_PERMANENT,
            BiometricConstants.ERROR_USER_CANCELED,
            BiometricConstants.ERROR_NO_BIOMETRICS,
            BiometricConstants.ERROR_HW_NOT_PRESENT,
            BiometricConstants.ERROR_NEGATIVE_BUTTON})
    @interface BiometricError {}
    public static class CryptoObject {
        private final Signature mSignature;
        private final Cipher mCipher;
        private final Mac mMac;

        public CryptoObject(@NonNull Signature signature) {
            mSignature = signature;
            mCipher = null;
            mMac = null;
        }

        public CryptoObject(@NonNull Cipher cipher) {
            mCipher = cipher;
            mSignature = null;
            mMac = null;
        }

        public CryptoObject(@NonNull Mac mac) {
            mMac = mac;
            mCipher = null;
            mSignature = null;
        }

        /**
         * Get {@link Signature} object.
         * @return {@link Signature} object or null if this doesn't contain one.
         */
        @Nullable
        public Signature getSignature() {
            return mSignature;
        }

        /**
         * Get {@link Cipher} object.
         * @return {@link Cipher} object or null if this doesn't contain one.
         */
        @Nullable
        public Cipher getCipher() {
            return mCipher;
        }

        /**
         * Get {@link Mac} object.
         * @return {@link Mac} object or null if this doesn't contain one.
         */
        @Nullable
        public Mac getMac() {
            return mMac;
        }
    }
    public static class AuthenticationResult {
        private final BiometricPromptV2.CryptoObject mCryptoObject;

        /**
         * @param crypto
         */
        AuthenticationResult(BiometricPromptV2.CryptoObject crypto) {
            mCryptoObject = crypto;
        }

        /**
         * Obtain the crypto object associated with this transaction
         */
        @Nullable
        public BiometricPromptV2.CryptoObject getCryptoObject() {
            return mCryptoObject;
        }
    }

    public abstract static class AuthenticationCallback {
        /**
         * Called when an unrecoverable error has been encountered and the operation is complete.
         * No further actions will be made on this object.
         * @param errorCode An integer identifying the error message. The error message will usually
         *                  be one of the BIOMETRIC_ERROR constants.
         * @param errString A human-readable error string that can be shown on an UI
         */
        public void onAuthenticationError(@BiometricError int errorCode,
                                          @NonNull CharSequence errString) {}

        /**
         * Called when a biometric is recognized.
         * @param result An object containing authentication-related data
         */
        public void onAuthenticationSucceeded(@NonNull AuthenticationResult result) {}

        /**
         * Called when a biometric is valid but not recognized.
         */

        public void onAuthenticationFailed() {}

        public void onAuthenticationChanged(){}
    }
    public static class PromptInfo {

        /**
         * A builder that collects arguments to be shown on the system-provided biometric dialog.
         */
        public static class Builder {
            private final Bundle mBundle = new Bundle();

            /**
             * Required: Set the title to display.
             */
            @NonNull
            public BiometricPromptV2.PromptInfo.Builder setTitle(@NonNull CharSequence title) {
                mBundle.putCharSequence(KEY_TITLE, title);
                return this;
            }

            /**
             * Optional: Set the subtitle to display.
             */
            @NonNull
            public BiometricPromptV2.PromptInfo.Builder setSubtitle(@Nullable CharSequence subtitle) {
                mBundle.putCharSequence(KEY_SUBTITLE, subtitle);
                return this;
            }
            public BiometricPromptV2.PromptInfo.Builder setFailHint(@NonNull CharSequence failHint) {
                mBundle.putCharSequence(KEY_FAIL_HINT, failHint);
                return this;
            }

            /**
             * Optional: Set the description to display.
             */
            @NonNull
            public BiometricPromptV2.PromptInfo.Builder setDescription(@Nullable CharSequence description) {
                mBundle.putCharSequence(KEY_DESCRIPTION, description);
                return this;
            }

            /**
             * Required: Set the text for the negative button. This would typically be used as a
             * "Cancel" button, but may be also used to show an alternative method for
             * authentication, such as screen that asks for a backup password.
             * @param text
             * @return
             */
            @NonNull
            public BiometricPromptV2.PromptInfo.Builder setNegativeButtonText(@NonNull CharSequence text) {
                mBundle.putCharSequence(KEY_NEGATIVE_TEXT, text);
                return this;
            }

            /**
             * Creates a {@link BiometricPromptV2}.
             * @return a {@link BiometricPromptV2}
             */
            @NonNull
            public BiometricPromptV2.PromptInfo build() {
                final CharSequence title = mBundle.getCharSequence(KEY_TITLE);
                final CharSequence negative = mBundle.getCharSequence(KEY_NEGATIVE_TEXT);

                if (TextUtils.isEmpty(title)) {
                    throw new IllegalArgumentException("Title must be set and non-empty");
                } else if (TextUtils.isEmpty(negative)) {
                    throw new IllegalArgumentException("Negative button text must be set and "
                            + "non-empty");
                }
                return new BiometricPromptV2.PromptInfo(mBundle);
            }
        }

        private Bundle mBundle;

        PromptInfo(Bundle bundle) {
            mBundle = bundle;
        }

        Bundle getBundle() {
            return mBundle;
        }

        @NonNull
        public CharSequence getTitle() {
            return mBundle.getCharSequence(KEY_TITLE);
        }


        @Nullable
        public CharSequence getSubtitle() {
            return mBundle.getCharSequence(KEY_SUBTITLE);
        }


        @Nullable
        public CharSequence getDescription() {
            return mBundle.getCharSequence(KEY_DESCRIPTION);
        }


        @NonNull
        public CharSequence getNegativeButtonText() {
            return mBundle.getCharSequence(KEY_NEGATIVE_TEXT);
        }
    }

    // Passed in from the client.
    final FragmentActivity mFragmentActivity;
    final Executor mExecutor;
    final BiometricPromptV2.AuthenticationCallback mAuthenticationCallback;

    // Created internally for devices before P.
    FingerprintDialogFragmentV2 mFingerprintDialogFragment;
    FingerprintHelperFragmentV2 mFingerprintHelperFragment;

    // Created internally for devices P and above.
    BiometricFragmentV2 mBiometricFragment;

    final DialogInterface.OnClickListener mNegativeButtonListener =
            new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                                CharSequence errorText =
                                        mBiometricFragment.getNegativeButtonText();
                                mAuthenticationCallback.onAuthenticationError(
                                        BiometricConstants.ERROR_NEGATIVE_BUTTON,
                                        errorText);
                                mBiometricFragment.cleanup();
                            } else {
                                CharSequence errorText =
                                        mFingerprintDialogFragment.getNegativeButtonText();
                                mAuthenticationCallback.onAuthenticationError(
                                        BiometricConstants. ERROR_NEGATIVE_BUTTON,
                                        errorText);
                                mFingerprintHelperFragment.cancel(
                                        FingerprintHelperFragmentV2
                                                .USER_CANCELED_FROM_NEGATIVE_BUTTON);
                            }
                        }
                    });
                }
            };
    private final LifecycleObserver mLifecycleObserver = new LifecycleObserver() {
        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        void onPause() {
            if (!mFragmentActivity.isChangingConfigurations()) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    if (mBiometricFragment != null) {
                        mBiometricFragment.cancel();
                    }
                } else {
                    // May be null if no authentication is occurring.
                    if (mFingerprintDialogFragment != null) {
                        mFingerprintDialogFragment.dismiss();
                    }
                    if (mFingerprintHelperFragment != null) {
                        mFingerprintHelperFragment.cancel(
                                FingerprintHelperFragmentV2.USER_CANCELED_FROM_NONE);
                    }
                }
            }
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
        void onResume() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                mBiometricFragment = (BiometricFragmentV2) mFragmentActivity
                        .getSupportFragmentManager().findFragmentByTag(BIOMETRIC_FRAGMENT_TAG);
                if (DEBUG) Log.v(TAG, "BiometricFragment: " + mBiometricFragment);
                if (mBiometricFragment != null) {
                    mBiometricFragment.setCallbacks(mExecutor, mNegativeButtonListener,
                            mAuthenticationCallback);
                }
            } else {
                mFingerprintDialogFragment = (FingerprintDialogFragmentV2) mFragmentActivity
                        .getSupportFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG);
                mFingerprintHelperFragment = (FingerprintHelperFragmentV2) mFragmentActivity
                        .getSupportFragmentManager().findFragmentByTag(
                                FINGERPRINT_HELPER_FRAGMENT_TAG);

                if (DEBUG) Log.v(TAG, "FingerprintDialogFragment: " + mFingerprintDialogFragment);
                if (DEBUG) Log.v(TAG, "FingerprintHelperFragment: " + mFingerprintHelperFragment);
                if (mFingerprintDialogFragment != null && mFingerprintHelperFragment != null) {
                    mFingerprintDialogFragment.setNegativeButtonListener(mNegativeButtonListener);
                    mFingerprintHelperFragment.setCallback(mExecutor, mAuthenticationCallback);
                    mFingerprintHelperFragment.setHandler(mFingerprintDialogFragment.getHandler());
                }
            }
        }
    };
    public BiometricPromptV2(@NonNull FragmentActivity fragmentActivity,
                           @NonNull Executor executor, @NonNull BiometricPromptV2.AuthenticationCallback callback) {
        if (fragmentActivity == null) {
            throw new IllegalArgumentException("FragmentActivity must not be null");
        }
        if (executor == null) {
            throw new IllegalArgumentException("Executor must not be null");
        }
        if (callback == null) {
            throw new IllegalArgumentException("AuthenticationCallback must not be null");
        }
        mFragmentActivity = fragmentActivity;
        mExecutor = executor;
        mAuthenticationCallback = callback;

        mFragmentActivity.getLifecycle().addObserver(mLifecycleObserver);
    }
    public void authenticate(@NonNull BiometricPromptV2.PromptInfo info, @NonNull BiometricPromptV2.CryptoObject crypto) {
        if (info == null) {
            throw new IllegalArgumentException("PromptInfo can not be null");
        } else if (crypto == null) {
            throw new IllegalArgumentException("CryptoObject can not be null");
        }
        authenticateInternal(info, crypto);
    }
    public void authenticate(@NonNull BiometricPromptV2.PromptInfo info) {
        if (info == null) {
            throw new IllegalArgumentException("PromptInfo can not be null");
        }
       if(mCipher==null){
           mCipher=CipherHelper.getInstance().createCipher();
       }
//        SharedPreferences sharedPreferences = mFragmentActivity.getSharedPreferences(DEFAULT_KEY_NAME, 0);
//       if(TextUtils.isEmpty(sharedPreferences.getString(HAS_FINGER_KEY,"")))
        if(CipherHelper.getInstance().initCipher(mCipher)){
            //change
            mAuthenticationCallback.onAuthenticationChanged();
            return;
        }
        BiometricPromptV2.CryptoObject cryptoObject=new BiometricPromptV2.CryptoObject(mCipher);
        authenticateInternal(info, cryptoObject/* crypto */);
    }

    private void authenticateInternal(@NonNull BiometricPromptV2.PromptInfo info, @Nullable BiometricPromptV2.CryptoObject crypto) {
        final Bundle bundle = info.getBundle();
        final FragmentManager fragmentManager = mFragmentActivity.getSupportFragmentManager();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            // Create the fragment that wraps BiometricPrompt once.
            if (mBiometricFragment == null) {
                mBiometricFragment = BiometricFragmentV2.newInstance();
            }
            mBiometricFragment.setCallbacks(mExecutor, mNegativeButtonListener,
                    mAuthenticationCallback);
            // Set the crypto object.
            mBiometricFragment.setCryptoObject(crypto);
            mBiometricFragment.setBundle(bundle);

            if (fragmentManager.findFragmentByTag(BIOMETRIC_FRAGMENT_TAG) == null) {
                // If the fragment hasn't been added before, add it. It will also start the
                // authentication.
                fragmentManager.beginTransaction().add(mBiometricFragment, BIOMETRIC_FRAGMENT_TAG)
                        .commitAllowingStateLoss();
            } else {
                // If it's been added before, just re-attach it.
                fragmentManager.beginTransaction().attach(mBiometricFragment).commitAllowingStateLoss();
            }
        } else {
            // Create the UI
            if (mFingerprintDialogFragment == null) {
                mFingerprintDialogFragment = FingerprintDialogFragmentV2.newInstance();
            }
            mFingerprintDialogFragment.setNegativeButtonListener(mNegativeButtonListener);
            mFingerprintDialogFragment.setBundle(bundle);
            mFingerprintDialogFragment.show(fragmentManager, DIALOG_FRAGMENT_TAG);

            // Create the connection to FingerprintManager
            if (mFingerprintHelperFragment == null) {
                mFingerprintHelperFragment = FingerprintHelperFragmentV2.newInstance();
            }
            mFingerprintHelperFragment.setCallback(mExecutor, mAuthenticationCallback);
            final Handler fingerprintDialogHandler = mFingerprintDialogFragment.getHandler();
            mFingerprintHelperFragment.setHandler(fingerprintDialogHandler);
            mFingerprintHelperFragment.setCryptoObject(crypto);
            fingerprintDialogHandler.sendMessageDelayed(
                    fingerprintDialogHandler.obtainMessage(
                            FingerprintDialogFragmentV2.DISPLAYED_FOR_500_MS), DELAY_MILLIS);

            if (fragmentManager.findFragmentByTag(FINGERPRINT_HELPER_FRAGMENT_TAG) == null) {
                // If the fragment hasn't been added before, add it. It will also start the
                // authentication.
                fragmentManager.beginTransaction()
                        .add(mFingerprintHelperFragment, FINGERPRINT_HELPER_FRAGMENT_TAG).commitAllowingStateLoss();
            } else {
                // If it's been added before, just re-attach it.
                fragmentManager.beginTransaction().attach(mFingerprintHelperFragment).commitAllowingStateLoss();
            }
        }
        // For the case when onResume() is being called right after authenticate,
        // we need to make sure that all fragment transactions have been committed.
        if(fragmentManager.isDestroyed()){
            return;
        }
        fragmentManager.executePendingTransactions();
    }
    public void cancelAuthentication() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            if (mBiometricFragment != null) {
                mBiometricFragment.cancel();
            }
        } else {
            if (mFingerprintHelperFragment != null && mFingerprintDialogFragment != null) {
                mFingerprintHelperFragment.cancel(
                        FingerprintHelperFragmentV2.USER_CANCELED_FROM_NONE);
                mFingerprintDialogFragment.dismiss();
            }
        }
    }

}
